/*
 * yocef - Lightweight IoC Event Framework
 *
 * Copyright (c) 2009, Yves Cuillerdier
 * All rights reserved.
 * 
 * This software is licensed under the new BSD License:
 * 
 * Redistribution and use in source and binary forms, with or without 
 * modification, are permitted provided that the following conditions are met:
 *
 * Redistributions of source code must retain the above copyright notice, 
 * this list of conditions and the following disclaimer.
 * 
 * Redistributions in binary form must reproduce the above copyright notice, 
 * this list of conditions and the following disclaimer in the documentation 
 * and/or other materials provided with the distribution.
 * 
 * Neither the name of the author nor the names of its contributors may 
 * be used to endorse or promote products derived from this software without 
 * specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE 
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 
 * POSSIBILITY OF SUCH DAMAGE.
 * 
 * @version 0.8.5
 */

package yocef.client;

import java.util.HashMap;
import java.util.Map;
import java.util.TreeMap;

/**
 * This class represents an association of an <code>Event</code> instance with a
 * <b>source</b> <code>Object</code>. This association is unique : Two different
 * instances of <code>EventBind</code> could not be <code>equals</code>
 * (represents the same <code>{Event,Object}</code> couple).
 */
public final class EventBind {

	/**
	 * A Map to enforce single instance of a given {Event,Object} couple
	 * <p>
	 * The main key is the source object to facilitate the <tt>off</tt>ing or
	 * cleaning of elements.
	 */
	private static Map<Object, Map<Event, EventBind>> bindStock = new HashMap<Object, Map<Event, EventBind>>();

	/** The Event and Source */
	private final Event event;
	private final Object source;

	/**
	 * Map that could be used to save/restore miscellaneous data. The main usage
	 * is to store objects build by the EventGlue to be later unregistered
	 */
	private final Map<String, Object> bindData = new TreeMap<String, Object>();

	/**
	 * The action to be performed in response to the event occurring on the
	 * source element.
	 */
	private EventAction action;

	/**
	 * The context for the
	 * {@link EventAction#handle(Event, Object, EventContext, EventParams)} when
	 * fired
	 */
	private EventContext context;

	/** Private constructor */
	private EventBind(final Event e, final Object s) {
		event = e;
		source = s;
	}

	/**
	 * EventBind factory. Ensure that two distinct <code>{Event,Object}</code>
	 * instances are not <code>equals</code>
	 * <p>
	 * Notice: this method is not synchronized. Must we ?
	 * 
	 * @param event
	 *            the event
	 * @param element
	 *            the source element
	 * @return A EventBind for this <tt>{Event,Object]</tt> couple, either an
	 *         old one (if the couple have already be binded), or a fresh one
	 *         (if we never see this couple before).
	 */
	public static EventBind get(final Event event, final Object element) {
		EventBind bind = null;
		Map<Event, EventBind> eb = bindStock.get(element);
		if (eb == null) {
			// First time we saw this source element
			eb = new TreeMap<Event, EventBind>();
			bindStock.put(element, eb);
		}
		bind = eb.get(event);
		if (bind == null) {
			// First time we saw this event for the element
			bind = new EventBind(event, element);
			eb.put(event, bind);
		}
		return bind;
	}

	/**
	 * EventBind factory. Ensure that two distinct <code>{Event,Object}</code>
	 * instances are not <code>equals</code>
	 * <p>
	 * Notice: this method is not synchronized. Must we ?
	 * 
	 * @param eventName
	 *            the event type the source element
	 * @return A EventBind for this <tt>{Event,Object]</tt> couple, either an
	 *         old one (if the couple have already be binded), or a fresh one
	 *         (if we never see this couple before).
	 */
	public static EventBind get(final String eventName, final Object element) {
		return get(Event.get(eventName), element);
	}

	/**
	 * Set the action for this binding. If an action is already defined, it will
	 * be replaced silently.
	 * 
	 * @param action
	 *            the action to performed when the event occurs on the element.
	 *            Must not be null
	 * @return this EventBind for easy chaining
	 * @see EventAction
	 */
	public EventBind perform(final EventAction anAction) {
		if (anAction == null) {
			throw new IllegalArgumentException("Action could not be null.");
		}
		action = anAction;
		return this;
	}

	/**
	 * Declare a context for the action when fired. If a context is already
	 * defined, it will be replaced silently.
	 * 
	 * @param ctx
	 *            an EventContext to be provided to the
	 *            {@link EventAction#handle(Event, Object, EventContext, EventParams)}
	 *            when called. Could be null to "erase" a previous context.
	 * @return this EventBind for easy chaining
	 * @see EventContext
	 */
	public EventBind using(final EventContext ctx) {
		context = ctx;
		return this;
	}

	/**
	 * Calling this method indicates that we are ready to process the event on
	 * the source object.
	 * <p>
	 * the source element is "physically" bind with the event type. From that
	 * moment, event could be received and handle.
	 * <p>
	 * The way the event is binded with the element is defined by an
	 * {@link EventGlue} instance associated with the source class (or a parent
	 * class of the source).
	 * <p>
	 * The link between the element and the event is done in the
	 * {@link EventGlue#accept(EventBind)} method.
	 * 
	 * @see EventGlue
	 */
	public void now() {
		/*
		 * Try to find a glue: Start from the source element class up through
		 * superclass until a glue registration is found and accept it.
		 */
		Class<?> classToBind = source.getClass();
		while (classToBind != null) {
			final EventGlue glue = EventGlue.get(classToBind);
			if (glue != null && glue.accept(this)) {
				return;
			}
			classToBind = classToBind.getSuperclass();
		}
		throw new RuntimeException(
				"No EventBinder found for class sourceObject.getClass()");
	}

	/**
	 * The Source will no more respond to the event. Perform housekeeping
	 * actions.
	 * <p>
	 * For internal use only!
	 */
	void unlink() {
		// Unregister listeners
		unGlue(this);
		// Removing itself from the bindStock is enough for the GC to be able to
		// reclaim the memory.
		final Map<Event, EventBind> eb = bindStock.get(source);
		if (eb != null) {
			eb.remove(event);
			if (eb.isEmpty()) {
				bindStock.remove(source);
			}
		}
	}

	/**
	 * Get the Event associated with this EventBind.
	 * 
	 * @return the event. Not null
	 */
	public Event getEvent() {
		return event;
	}

	/**
	 * Get the source Object associated with this EventBind.
	 * 
	 * @return the source object. Not null
	 */
	public Object getSource() {
		return source;
	}

	/**
	 * Get the Action associated with this EventBind.
	 * 
	 * @return the action. Normally not null, except if we forgot to set it with
	 *         the {@link EventBind#perform(EventAction)} method
	 */
	public EventAction getAction() {
		return action;
	}

	/**
	 * Get the context associated with this EventBind.
	 * 
	 * @return the context. Could be null
	 */
	public EventContext getContext() {
		return context;
	}

	/**
	 * Utility method to unGlue a binding
	 * 
	 * @param binding
	 *            the binding to unGlue
	 */
	private static void unGlue(final EventBind binding) {
		Class<?> classToUnbind = binding.source.getClass();
		while (classToUnbind != null) {
			final EventGlue glue = EventGlue.get(classToUnbind);
			if (glue != null && glue.release(binding)) {
				return;
			}
			classToUnbind = classToUnbind.getSuperclass();
		}
	}

	/**
	 * Objects don't know anything about the events that are set on them. When
	 * an object is no more referred anywhere outside the event framework,
	 * references remaining in this framework prevent the garbage collector to
	 * reclaim the memory.
	 * <p>
	 * This method will remove references that could prevent the object to be
	 * reclaim by the GC.
	 * <p>
	 * Don't call it. Internal use only
	 * 
	 * @param element
	 *            the element to "clean"
	 * @see Event#cleanup(Object)
	 */
	static void cleanup(final Object element) {
		final Map<Event, EventBind> eb = bindStock.get(element);
		if (eb == null) {
			return;
		}
		// Perform housekeeping for all bindings
		for (final EventBind bind : eb.values()) {
			unGlue(bind);
		}
		// Remove the whole map associated with the element
		bindStock.remove(element);
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see java.lang.Object#hashCode()
	 */
	@Override
	public int hashCode() {
		// return eventAndSource.hashCode();
		return source.hashCode();
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see java.lang.Object#equals(java.lang.Object)
	 */
	@Override
	public boolean equals(final Object obj) {
		// Two distinct instances cannot be equals
		return (this == obj);
	}

	/**
	 * Save a data object in this EventBing. This method is typically used to
	 * store handler reference in {@link EventGlue} for native events. The
	 * instance could be retrieved by {@link EventBind#getData(String)}.
	 * 
	 * @param key
	 *            the key for the object
	 * @param object
	 *            the object to save
	 */
	public void addData(final String key, final Object object) {
		bindData.put(key, object);
	}

	/**
	 * Get and <b>remove</b> the object associated with the key.
	 * 
	 * @param key
	 *            the object key
	 * @return the object associated with the key
	 */
	public Object getData(final String key) {
		return bindData.remove(key);
	}
}
